package hydrap2p.library;

import hydrap2p.helper.RangeUtils;
import hydrap2p.library.listening.ListenerManager;
import hydrap2p.logging.Level;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;


/**
 * Does most of the functions dealing with data about Shared Objects
 * @author Walter Huf
 *
 */
public final class Data {
	private SafeDatabase db;
	private volatile static Data instance;
	private FileAccess files;
	private ListenerManager listeners;
	private DownloadHashChecker hashchecker;
	
	private Data() throws Exception
	{
		try {
			db=SafeDatabase.getInstance();
		} catch (SQLException e) {
			e.printStackTrace();
			throw new Exception("Could not connect to database");
		}
		files=new FileAccess();
		listeners=ListenerManager.getInstance();
		hashchecker=new DownloadHashChecker();
		new Thread(hashchecker).start();
	}
	public static Data getInstance() throws Exception
	{
		if (instance==null){
			synchronized(Data.class)
			{
				if (instance==null)
					instance=new Data();
			}
		}
		return instance;
	}
	public static void shutdown()
	{
		try {
		getInstance().hashchecker.stopRunning();
		while (getInstance().hashchecker.isRunning());
		} catch (Exception e){}
		
		instance=null;
	}
	
	public static void log(Level level, String source, String reason)
	{
		LibraryLogging.log(level, source, reason);
	}
	
	public boolean isScanning()
	{
		return hashchecker.isScanning();
	}
	
	public long createFile(String path, long size) throws FileAlreadyExists,IOException {
		try {
			files.create(path);
		} catch (IOProblem e) {
			throw new IOException("Could not create file "+path);
		}
		File temp=new File(path);
		if (temp.exists())
		{
			ItemInfo nein=db.create();
			nein.isDirectory=false;
			nein.pathname=temp.getParent();
			nein.name=temp.getName();
			nein.size=size;
			nein.isShared=false;
			db.save(nein);
			listeners.newSharedItem(nein);
			return nein.uid;
		}
		log(Level.FATAL, "Library - Create File", "Did not create new file on the disk: "+path);
		throw new IOException("Could not create file "+path);
	}
	
	public long createFileSet(String name, List<Long> uids) throws InvalidUID {
		ItemInfo temp=db.create();
		temp.name=name;
		temp.children=uids;
		temp.isDirectory=true;
		temp.size=0;
		for (long uid:uids)
		{
			SharedItemInfo item=getSharedItemInfo(uid);
			if (item==null)
				throw new InvalidUID(uid);
			temp.size+=item.size;
		}
		db.save(temp);
		listeners.newSharedItem(temp);
		return temp.uid;
	}
	
	public void deleteDownloadHash(long uid, HashData hash)
	{
		ItemInfo temp=db.load(uid,true);
		temp.targetHashesEx.remove(hash);
		db.save(temp);
	}
	public boolean deleteExtraData(long uid, String key)
	{
		ItemInfo temp=db.load(uid,true);
		if (temp.extra.containsKey(key))
		{
			temp.extra.remove(key);
			db.save(temp);
			listeners.changedExtra(temp);
			return true;
		}
		return false;
	}
	public boolean deleteMetadata(long uid, String key)
	{
		ItemInfo temp=db.load(uid,true);
		if (temp.metadata.containsKey(key))
		{
			temp.metadata.remove(key);
			db.save(temp);
			listeners.changedMetadata(temp);
			return true;
		}
		return false;
	}
	public boolean deleteFile(long uid)
	{
		boolean ret=false;
		ItemInfo temp=db.load(uid, true);
		if (temp!=null)
			ret=files.delete(temp);
		if (ret)
		{
			db.delete(temp);
			listeners.removedSharedItem(temp);
		}
		return ret;
	}
	public List<SharedItemInfo> getChildren(long uid) 
	{
		ItemInfo temp=db.load(uid,false);
		if (temp!=null)
		{
			List<SharedItemInfo> ret=new LinkedList<SharedItemInfo>();
			for (Long t:temp.children)
			{
				ItemInfo t2=db.load(t,false);
				if (t2!=null)
					ret.add(t2);
			}
			return ret;
		}
		return null;
	}
	public List<Range> getDownloadedRanges(long uid) {
		return db.getDownloadedRanges(uid);
	}
	public List<Range> getVerifiedRanges(long uid){
		return db.getVerifiedRanges(uid);
	}
	public SharedItemInfo getSharedItemInfo(long uid) {
		return db.load(uid,false);
	}
	public void setShared(long uid, boolean shared) {
		ItemInfo item=db.load(uid, true);
		boolean oldshared=item.isShared;
		item.isShared=shared;
		db.save(item);
		if (!oldshared && shared)
			listeners.newSharedItem(item);
		else if (oldshared && !shared)
			listeners.removedSharedItem(item);
	}
	public void setDownloadHash(long uid, HashData hash) {
		ItemInfo temp=db.load(uid,true);
		if (!temp.targetHashesEx.contains(hash))
		{
			temp.targetHashesEx.add(new HashDataEx(hash,false,false));
			db.save(temp);
			hashchecker.enqueue(uid);
		}
		db.release(temp);
	}
	public void setExtraData(long uid, String key, String value){
		ItemInfo temp=db.load(uid,true);
		temp.extra.put(key,value);
		db.save(temp);
		listeners.changedExtra(temp);
	}
	public boolean setMetadata(long uid, String key, String value) {
		ItemInfo temp=db.load(uid,true);
		temp.metadata.put(key,value);
		db.save(temp);
		listeners.changedMetadata(temp);
		return true;
	}
	
	public boolean saveData(long uid, long offset, byte[] data, int dataOffset, int dataLen) throws FileNotFoundException, IOException, InvalidUID
	{
		ItemInfo nein=null;
		try {
			nein=db.load(uid, true);
			if (nein==null)
				throw new InvalidUID(uid);
			
			if (!nein.isDirectory)
			{
				files.saveData(nein, offset, data, dataOffset, dataLen);
			}
			else
			{
				long seeked=0;
				for (long childuid:nein.children)
				{
					ItemInfo child=db.load(childuid, false);
					if (child==null)
						throw new InvalidUID(childuid);
					
					if (offset>=seeked && offset<seeked+child.size)
					{
						int amounttowrite=(int)Math.min(dataLen, Math.min(Integer.MAX_VALUE,seeked+child.size-offset));
						saveData(childuid, offset-seeked, data, dataOffset, amounttowrite);
						dataOffset+=amounttowrite;
						dataLen-=amounttowrite;
						offset+=amounttowrite;
					}
					seeked+=child.size;
				}
			}

			Range current=new Range(offset,data.length);
			
			List<Range> downloadedRanges=db.getDownloadedRanges(uid);
			downloadedRanges=RangeUtils.merge(downloadedRanges, current);
			db.setDownloadedRanges(uid, downloadedRanges);
			
			List<Range> verifiedRanges=db.getVerifiedRanges(uid);
			if (RangeUtils.containsPartial(verifiedRanges, current))
			{
				boolean changed=false;
				for (HashDataEx hash:nein.targetHashesEx)
				{
					if (RangeUtils.contains(hash.range, current))
					{
						hash.isVerified=false;
						changed=true;
					}
				}
				if (changed)
					db.save(nein);
				
				verifiedRanges=RangeUtils.remove(verifiedRanges, current);
				db.setVerifiedRanges(uid, verifiedRanges);
				listeners.deleteVerifiedProgress(nein, current);
			}
			listeners.newPartialProgress(nein, current);
			hashchecker.enqueue(uid);
		}
		finally {
			if (nein!=null)
				db.release(nein);
		}
		return true;
	}

	public byte[] loadData(long uid, long offset, int len) throws FileNotFoundException, IOException
	{
		ItemInfo nein=null;
		try {
			nein=db.load(uid, true);
			if (nein!=null)
				return files.loadData(nein, offset, len);
			return null;
		}
		finally {
			if (nein!=null)
				db.release(nein);
		}
	}
	public List<SharedItemInfo> searchExactFilename(String path,String filename) {
		List<SharedItemInfo> ret=new LinkedList<SharedItemInfo>();
		for (ItemInfo temp : db.searchExactPathFilename(path,filename))
			ret.add(temp);
		return ret;
	}
	public List<SharedItemInfo> searchFilename(String path, String filename) {
		List<SharedItemInfo> ret=new LinkedList<SharedItemInfo>();
		for (ItemInfo temp : db.searchPathFilename(path,filename))
			ret.add(temp);
		return ret;
	}
	public List<SharedItemInfo> searchHash(String algorithm, String hashdata) {
		List<SharedItemInfo> ret=new LinkedList<SharedItemInfo>();
		for (ItemInfo temp : db.searchHash(algorithm, hashdata))
			ret.add(temp);
		return ret;
	}
	
	public void setFileName(long uid, String name) throws FileAlreadyExists {
		ItemInfo item=null;
		try {
			item=db.load(uid, true);
			listeners.removedSharedItem(item);
			try {
				files.move(item, item.pathname, name);
				item.name=name;
				db.save(item);
			} catch (FileAlreadyExists e) {
				throw e;
			} catch (IOException e) {
				LibraryLogging.log(Level.MISHAP, "Library - Rename File", "Failed to rename file: "+e.getLocalizedMessage());
			} catch (IOProblem e) {
				LibraryLogging.log(Level.MISHAP, "Library - Rename File", "Failed to rename file: "+e.message);
			}
			listeners.newSharedItem(item);
		}
		finally {
			if (item!=null)
				db.release(item);
		}
	}
	
	public void setPathName(long uid, String path) throws FileAlreadyExists {
		ItemInfo item=null;
		try {
			item=db.load(uid, true);
			listeners.removedSharedItem(item);
			try {
				files.move(item, path, item.name);
				item.pathname=path;
				db.save(item);
			} catch (FileAlreadyExists e) {
				throw e;
			} catch (IOException e) {
				LibraryLogging.log(Level.MISHAP, "Library - Move File", "Failed to move file: "+e.getLocalizedMessage());
			} catch (IOProblem e) {
				LibraryLogging.log(Level.MISHAP, "Library - Rename File", "Failed to move file: "+e.message);
			}
			listeners.newSharedItem(item);
		}
		finally {
			if (item!=null)
				db.release(item);
		}
	}
}
